local UnlimitTransitionCount = 0
local UpwardTransitionEnable = 1
local DownwardTransitionEnable = 2

local CombatSpellDisable = 0
local CombatSpellEnable = 1
local CombatSpellInstant = 2

local EnterCombatStatus = function(status)
    status.enterCombatTime = systime()
    status.combatSpellWeights = {}
    status.history = {}
    status.stage = 0
end

local LeaveCombatStatus = function(status)
    status.enterCombatTime = nil
    status.combatSpellWeights = nil
    status.history = nil
    status.stage = nil
    status.comboCastCounter = nil
    status.lastSpellID, status.lastSpellLinkID = nil, nil
    status.spellID, status.spellLv, status.isSpellLink = nil, nil, nil
end

local IsTransitionSpellAvailable = function(status, spellID, spellFlags)
    if (spellFlags & (1 << status.transit)) == 0 then
        return false
    end
    if (spellFlags & (1 << UnlimitTransitionCount)) ~= 0 then
        return true
    end
    if not status.history[spellID] then
        return true
    end
end

local CheckCombatSpellAvailable =
    function(status, spellID, spellCombo, spellBurst, spellMaxCnt, nowTime)
    if status.lastSpellID == spellID then
        if spellCombo ~= 0 and spellCombo <= (status.comboCastCounter or 0) then
            return CombatSpellDisable
        end
    end
    local history = status.history[spellID]
    if history then
        if spellMaxCnt ~= 0 and spellMaxCnt <= history.totalCastCounter then
            return CombatSpellDisable
        end
        if spellBurst ~= 0 and history.lastCastTime + spellBurst < nowTime then
            return CombatSpellInstant
        end
        return CombatSpellEnable
    else
        if spellBurst ~= 0 and status.enterCombatTime + spellBurst < nowTime then
            return CombatSpellInstant
        end
        return CombatSpellEnable
    end
end

local CheckCombatSpellCastable = function(obj, spellID, spellLv, spellStatus)
    if spellStatus ~= CombatSpellDisable then
        local errCode = CanCastWithoutLearnSpell(
            obj, spellID, spellLv, CanCastSpellFlag.CheckDistance)
        if errCode ~= CommonSuccess then
            return CombatSpellDisable
        end
    end
    return spellStatus
end

local TryCastNextCombatSpell = function(obj, status, cfg)
    local nowTime = systime()
    local weights = status.combatSpellWeights
    local spells = cfg.combat[status.stage].spells
    for i, spell in ipairs(spells) do
        local spellID, spellLv, spellWeight,
            spellCombo, spellBurst, spellMaxCnt = table.unpack(spell)
        local spellStatus = CheckCombatSpellCastable(obj, spellID, spellLv,
            CheckCombatSpellAvailable(status, spellID, spellCombo,
                spellBurst,spellMaxCnt, nowTime))
        if spellStatus == CombatSpellInstant then
            return spellID, spellLv
        elseif spellStatus == CombatSpellEnable then
            weights[i] = spellWeight
        else
            weights[i] = 0
        end
    end
    weights[#spells+1] = nil
    local i = FastRandomByWeightValue(weights)
    if i and i >= 1 then
        return spells[i][1], spells[i][2]
    end
end

local TryCastNextPriorSpell = function(prior)
    if #prior ~= 0 then
        local spellID = table.remove(prior, 1)
        local spellLv = table.remove(prior, 1)
        return spellID, spellLv
    end
end

local TryCastNextLinkSpell = function(status, cfg, lastSpellID)
    local links = cfg.combat[status.stage].links
    if not links then
        return
    end
    for _, link in ipairs(links) do
        for i = 1, #link, 2 do
            if link[i] == lastSpellID then
                return link[i+2], link[i+3]
            end
        end
    end
end

local TryCastNextTransitionSpell = function(status, cfg, lastSpellID)
    local transitions = cfg.combat[status.stage].transitions
    if not transitions then
        return
    end
    local lastTransitIndex = not lastSpellID and 0 or nil
    if lastSpellID then
        for i = 1, #transitions do
            if transitions[i][1] == lastSpellID then
                lastTransitIndex = i
                break
            end
        end
    end
    if lastTransitIndex then
        for i = lastTransitIndex+1, #transitions do
            local spellID, spellLv, spellFlags = table.unpack(transitions[i])
            if IsTransitionSpellAvailable(status, spellID, spellFlags) then
                return spellID, spellLv
            end
        end
    end
end

local TryCastNextSpell = function(obj, vars, status, prior, cfg)
    local lastSpellID = status.lastSpellID
    local stage = cfg.evaluate(obj, vars)
    if stage == status.stage then
        if status.transit then
            local spellID, spellLv =
                TryCastNextTransitionSpell(status, cfg, lastSpellID)
            if spellID then
                return spellID, spellLv
            else
                status.transit = nil
            end
        else
            local spellID, spellLv =
                TryCastNextLinkSpell(status, cfg, lastSpellID)
            if spellID then
                return spellID, spellLv, true
            end
        end
    else
        status.stage, status.transit = stage, stage < status.stage and
            UpwardTransitionEnable or DownwardTransitionEnable
        local spellID, spellLv = TryCastNextTransitionSpell(status, cfg)
        if spellID then
            return spellID, spellLv
        else
            status.transit = nil
        end
    end
    local spellID, spellLv = TryCastNextPriorSpell(prior)
    if spellID then
        return spellID, spellLv
    end
    local spellID, spellLv = TryCastNextCombatSpell(obj, status, cfg)
    if spellID then
        return spellID, spellLv
    end
end

local DoCastNextSpell = function(obj, status)
    local errCode = CastWithoutLearnSpell(obj, status.spellID, status.spellLv)
    if errCode ~= CommonSuccess then
        print(string.format('[%d]`%s` cast spell `%d`,`%d` failed.',
            obj:GetEntry(), obj:GetName(), status.spellID, status.spellLv))
    end
    local history = status.history[status.spellID]
    if not history then
        history = {totalCastCounter=0, lastCastTime=0}
        status.history[status.spellID] = history
    end
    history.totalCastCounter = history.totalCastCounter + 1
    history.lastCastTime = systime()
    if status.lastSpellID ~= status.spellID then
        status.lastSpellID = status.spellID
        status.comboCastCounter = 1
    else
        status.comboCastCounter = status.comboCastCounter + 1
    end
    status.spellID = nil
end

function AISampleCombatNode(bb, cfg)
    local node, vars = AIScriptableNode(bb), bb.variables
    local status, prior = vars['combat.status?'], vars['combat.prior?']
    node:SetKernelScriptable(function(obj)
        if not status.spellID then
            status.spellID, status.spellLv, status.isSpellLink =
                TryCastNextSpell(obj, vars, status, prior, cfg)
        end
        if not status.spellID then
            obj:MoveToCombatPosition(0.01, 0.01)
            return AINodeStatus.Finished
        end
        DoCastNextSpell(obj, status)
        return AINodeStatus.Finished
    end)
    return node
end

function AISampleInitBehaviorTree(obj, bb, cfg)
    local vars = bb.variables
    local status = NewIndexTable(vars, 'combat.status?')
    local prior = NewIndexTable(vars, 'combat.prior?')
    local t = {}
    t.events = {
        ObjectHookEvent.OnUnitChangeCombatStatus,
    }
    t.OnUnitChangeCombatStatus = function(isInCombat)
        if isInCombat then
            EnterCombatStatus(status)
        else
            LeaveCombatStatus(status)
        end
    end
    obj:AttachObjectHookInfo(t)
end